/*           Copyright(c) 2005-2008 Dave Sexton            *
 *                   HtmlEditor Control                    *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using DaveSexton.Controls;
using System.Windows.Forms;
using System.Net;
using System.Drawing;
using System.IO;
using System.ComponentModel;
using mshtml;
using DaveSexton.Controls.Resources;
using System.Xml;

namespace DaveSexton.Controls
{
	public enum ImporterRebaseAction
	{
		None,
		TargetDirectory,
		RebaseDirectory
	}

	public sealed class Importer : Component
	{
		#region Public Properties
		/// <summary>
		/// Gets or sets the directory to where downloaded resources will be copied.
		/// </summary>
		[Category("Misc"), Description("Indicates the directory to where downloaded resources will be copied.")]
		public string TargetDirectory
		{
			get
			{
				return targetDirectory;
			}
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");

				targetDirectory = value;
			}
		}

		/// <summary>
		/// Gets or sets whether imported HTML elements are rebased.
		/// </summary>
		[Category("Behavior"), DefaultValue(ImporterRebaseAction.None), Description("Indicates whether imported HTML elements are rebased.")]
		public ImporterRebaseAction Rebase
		{
			get
			{
				return rebase;
			}
			set
			{
				if (!Enum.IsDefined(typeof(ImporterRebaseAction), value))
					throw new InvalidEnumArgumentException("value", (int) value, typeof(ImporterRebaseAction));

				rebase = value;
			}
		}

		/// <summary>
		/// Gets or sets the directory to which all imported HTML elements are rebased when the <see cref="Rebase"/> property is set to <see cref="ImporterRebaseAction.RebaseDirectory"/>.
		/// </summary>
		[Category("Misc"), Description("Indicates the directory to which all imported HTML elements are rebased when the Rebase property is set to RebaseDirectory.")]
		public string RebaseDirectory
		{
			get
			{
				return rebaseDirectory;
			}
			set
			{
				rebaseDirectory = value;
			}
		}

		[Browsable(false)]
		public IDictionary<string, Exception> ImportErrors
		{
			get
			{
				return errors;
			}
		}

		[Browsable(false)]
		public bool HasErrors
		{
			get
			{
				return errors.Count > 0;
			}
		}
		#endregion

		#region Private / Protected
		private string targetDirectory, rebaseDirectory;
		private ImporterRebaseAction rebase = ImporterRebaseAction.None;
		private readonly IDictionary<string, Exception> errors = new Dictionary<string, Exception>();
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="Importer" /> class.
		/// </summary>
		public Importer()
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="Importer" /> class.
		/// </summary>
		public Importer(string targetDirectory)
		{
			if (targetDirectory == null)
				throw new ArgumentNullException("targetDirectory");

			this.targetDirectory = targetDirectory;
		}
		#endregion

		#region Methods
		private static bool ImportResource(Uri source, string target)
		{
			if (!source.IsAbsoluteUri || source.IsFile || source.IsUnc)
			{
				FileInfo sourceInfo = new FileInfo((source.IsAbsoluteUri) ? source.LocalPath : source.ToString());
				FileInfo targetInfo = new FileInfo(target);

				if (!sourceInfo.FullName.Equals(targetInfo.FullName, StringComparison.OrdinalIgnoreCase))
				{
					sourceInfo.CopyTo(targetInfo.FullName, true);
					return true;
				}
			}
			else
			{
				Uri targetUri;
				if (!Uri.TryCreate(target, UriKind.RelativeOrAbsolute, out targetUri) || source != targetUri)
				{
					using (Image image = Bitmap.FromStream(NativeMethods.GetUrlCacheEntry(
						source.GetComponents(UriComponents.SchemeAndServer | UriComponents.PathAndQuery, UriFormat.Unescaped))))
					{
						image.Save(target);
						return true;
					}
				}
			}

			return false;
		}

		private static void RebaseElement(IHTMLElement element, string pathAttributeName, string path)
		{
			string src = element.getAttribute(pathAttributeName, 1) as string;

			src = System.Web.HttpUtility.UrlDecode(src);

			RebaseElement(element, pathAttributeName, path, Path.GetFileName(src));
		}

		private static void RebaseElement(IHTMLElement element, string pathAttributeName, string path, string name)
		{
			element.setAttribute(pathAttributeName, CombinePath(path, name), 1);
		}

		private static void RebaseAttribute(XmlAttribute attribute, string path, string name)
		{
			attribute.Value = CombinePath(path, name);
		}

		private static string CombinePath(string path, string name)
		{
			if (Path.IsPathRooted(path))
				path = Path.Combine(path, name);
			else
			// format relative path (without leading /)
			{
        if (path.StartsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
          || path.StartsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal))
					path = path.Substring(1);

				string root = path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

				if (root.Length == 0)
					return name;
				else if (root[root.Length - 1] == Path.AltDirectorySeparatorChar)
					return root + name;
				else
					return root + Path.AltDirectorySeparatorChar + name;
			}

			return new Uri(path, UriKind.RelativeOrAbsolute).ToString();
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "1#")]
		public static int RebaseImages(string path, ref string htmlBody)
		{
			if (string.IsNullOrEmpty(path))
				throw new ArgumentException(Errors.NullOrEmpty, "path");

			if (string.IsNullOrEmpty(htmlBody))
				return 0;

			HTMLDocumentClass document = new HTMLDocumentClass();

			using (HtmlStream stream = new HtmlStream(document, false, HtmlStreamAccess.Write))
			{
				stream.Write(
					@"<!DOCTYPE html PUBLIC ""-//W3C//DTD XHTML 1.0 Transitional//EN"" ""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"">" +
					@"<html xmlns=""http://www.w3.org/1999/xhtml""><head><title>Html Editor Resource Importer</title></head><body></body></html>");
			}

			while (!"complete".Equals(document.readyState, StringComparison.OrdinalIgnoreCase))
				// the document is loaded asynchronously
				Application.DoEvents();

			document.body.innerHTML = htmlBody;

			int rebased = 0;

			foreach (IHTMLElement image in document.images)
			{
				rebased++;
				RebaseElement(image, "src", path);
			}

			if (rebased > 0)
				htmlBody = document.body.innerHTML;

			return rebased;
		}

		public static int RebaseImages(string path, HtmlDocument document)
		{
			if (string.IsNullOrEmpty(path))
				throw new ArgumentException(Errors.NullOrEmpty, "path");

			if (document == null)
				throw new ArgumentNullException("document");

			int rebased = 0;

			foreach (HtmlElement image in document.Images)
			{
				rebased++;
				RebaseElement((IHTMLElement) image.DomElement, "src", path);
			}

			return rebased;
		}

		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		public int RebaseImagesToTargetDirectory(HtmlDocument document)
		{
			return RebaseImages(targetDirectory, document);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
		public int RebaseImagesToTargetDirectory(ref string htmlBody)
		{
			return RebaseImages(targetDirectory, ref htmlBody);
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#"),
		System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public int ImportXhtmlImages(ref string xhtmlBody)
		{
			if (string.IsNullOrEmpty(targetDirectory))
				throw new InvalidOperationException(Errors.TargetDirectoryInvalid);

			if (string.IsNullOrEmpty(xhtmlBody))
				return 0;

			XmlDocument document = new XmlDocument();
			document.PreserveWhitespace = true;
			document.LoadXml("<root>" + xhtmlBody + "</root>");

			int count = 0;

			errors.Clear();

			foreach (XmlAttribute image in document.SelectNodes("//img/@src"))
			{
				try
				{
					if (ImportResourceFromAttribute(image))
						count++;
				}
				catch (Exception ex)
				{
					errors.Add(image.Value, ex);
				}
			}

			if (count > 0)
				xhtmlBody = document.DocumentElement.InnerXml;

			return count;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#"), 
		System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public int ImportImages(ref string htmlBody)
		{
			if (string.IsNullOrEmpty(targetDirectory))
				throw new InvalidOperationException(Errors.TargetDirectoryInvalid);

			if (string.IsNullOrEmpty(htmlBody))
				return 0;

			HTMLDocumentClass document = new HTMLDocumentClass();

			using (HtmlStream stream = new HtmlStream(document, false, HtmlStreamAccess.Write))
			{
				stream.Write(
					@"<!DOCTYPE html PUBLIC ""-//W3C//DTD XHTML 1.0 Transitional//EN"" ""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"">" +
					@"<html xmlns=""http://www.w3.org/1999/xhtml""><head><title>Html Editor Resource Importer</title></head><body>" + htmlBody + "</body></html>");
			}

			while (!"complete".Equals(document.readyState, StringComparison.OrdinalIgnoreCase))
				// the document is loaded asynchronously
				Application.DoEvents();

			int count = 0;

			errors.Clear();

			foreach (IHTMLElement image in document.images)
			{
				try
				{
					if (ImportElement(image, "src"))
						count++;
				}
				catch (Exception ex)
				{
					errors.Add(((IHTMLImgElement) image).src, ex);
				}
			}

			if (count > 0)
				htmlBody = document.body.innerHTML;

			return count;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public int ImportImages(HtmlDocument document)
		{
			if (string.IsNullOrEmpty(targetDirectory))
				throw new InvalidOperationException(Errors.TargetDirectoryInvalid);

			if (document == null)
				throw new ArgumentNullException("document");

			int count = 0;

			errors.Clear();

			foreach (HtmlElement image in document.Images)
			{
				try
				{
					if (ImportElement(image, "src"))
						count++;
				}
				catch (Exception ex)
				{
					errors.Add(image.GetAttribute("src"), ex);
				}
			}

			return count;
		}

		public bool ImportImage(HtmlElement image)
		{
			if (string.IsNullOrEmpty(targetDirectory))
				throw new InvalidOperationException(Errors.TargetDirectoryInvalid);

			if (image == null)
				throw new ArgumentNullException("image");

			if (!(image.DomElement is IHTMLImgElement))
				throw new ArgumentException(Errors.InvalidHtmlElementNotImg, "image");

			return ImportElement(image, "src");
		}

		/// <param name="pathAttributeName">Case-sensitive name of the attribute that indicates the resource's path.</param>
		private bool ImportElement(HtmlElement element, string pathAttributeName)
		{
			return ImportElement((IHTMLElement) element.DomElement, pathAttributeName);
		}

		/// <param name="pathAttributeName">Case-sensitive name of the attribute that indicates the resource's path.</param>
		private bool ImportElement(IHTMLElement element, string pathAttributeName)
		{
			string path = element.getAttribute(pathAttributeName, 1) as string;

			if (!string.IsNullOrEmpty(path))
			{
				Uri source;
				Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out source);

				if (string.Equals(source.Scheme, "about", StringComparison.OrdinalIgnoreCase))
					source = new Uri(source.LocalPath, UriKind.RelativeOrAbsolute);

				string name = Path.GetFileName((source.IsAbsoluteUri) ? source.GetComponents(UriComponents.Path, UriFormat.Unescaped) : source.ToString());

				ImportResource(source, Path.Combine(targetDirectory, name));

				switch (rebase)
				{
					case ImporterRebaseAction.TargetDirectory:
						RebaseElement(element, pathAttributeName, targetDirectory, name);
						break;
					case ImporterRebaseAction.RebaseDirectory:
						RebaseElement(element, pathAttributeName, rebaseDirectory, name);
						break;
				}
				
				// true must be returned even if ImportResource had returned false since the
				// element may be rebased and the caller may count on the return value to 
				// determine whether it must save the changes
				return true;
			}

			return false;
		}

		private bool ImportResourceFromAttribute(XmlAttribute attribute)
		{
			string path = attribute.Value;

			if (!string.IsNullOrEmpty(path))
			{
				Uri source;
				Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out source);

				string name = Path.GetFileName((source.IsAbsoluteUri) ? source.GetComponents(UriComponents.Path, UriFormat.Unescaped) : source.ToString());

				ImportResource(source, Path.Combine(targetDirectory, name));

				switch (rebase)
				{
					case ImporterRebaseAction.TargetDirectory:
						RebaseAttribute(attribute, targetDirectory, name);
						break;
					case ImporterRebaseAction.RebaseDirectory:
						RebaseAttribute(attribute, rebaseDirectory, name);
						break;
				}

				// true must be returned even if ImportResource had returned false since the
				// attribute may be rebased and the caller may count on the return value to 
				// determine whether it must save the changes
				return true;
			}

			return false;
		}
		#endregion
	}
}
